using System;
using System.Collections.Generic;
using System.Linq;
using DecisionMaking.Domain;
using Jint;
using Jint.Expressions;
using log4net;

namespace DecisionMaking.BusinessLogic.Algorithms
{
    /// <summary>
    /// An IAlgorithm implementation that uses the Javascript Interpreter as an execution engine. The code for execution
    /// is taken from the AlgorithmDefinition.
    /// 
    /// The following parameters are exposed to the algorithm itself:
    /// agent - the agent performing the decision making
    /// cues - the list of cues in the environment
    /// left, right - the two candidates to decide upon
    /// 
    /// The algorithm is expected to return a struct of type AlgorithmResult
    /// </summary>
    public class JintAlgorithm : IAlgorithm
    {
        private AlgorithmDefinition Definition { get; set; }

        private Program _program;

        private static readonly ILog Log = LogManager.GetLogger(typeof(JintAlgorithm));

        /// <summary>
        /// Used to ensure each thread uses its own engine instance since JintEngine is not thread-safe
        /// </summary>
        [ThreadStatic]
        private static JintEngine _engine = new JintEngine();
        private static JintEngine Engine
        {
            get
            {
                if (_engine == null)
                {
                    _engine = new JintEngine();
                }
                return _engine;
            }
        }

        public AlgorithmResult Execute(Agent agent, IList<Cue> cues, Candidate left, Candidate right)
        {
            object result;

            lock (Engine)
            {
                try
                {
                    result = Engine
                        .SetParameter("agent", agent)
                        .SetParameter("cues", cues)
                        .SetParameter("left", left)
                        .SetParameter("right", right)
                        .SetParameter("logger", Log)
                        .Run(_program);
                }
                catch (JintException e)
                {
                    string message = string.Format("Error while running algorithm {0}: {1}", Definition.Name, e.Message);
                    Log.Error(message, e);
                    throw new AlgorithmExecutionException(message, e);
                }
            }
            try
            {
                return (AlgorithmResult)result;
            }
            catch (InvalidCastException e)
            {
                throw new AlgorithmExecutionException(string.Format("Bad return type; expected AlgorithmResult, got {0}", result.GetType()), e);
            }
        }

        public string Validate()
        {
            string errors;

            var hasErrors = JintEngine.HasErrors(Definition.Code, out errors);

            return hasErrors ? errors : null;
        }

        public AlgorithmDefinition GetDefinition()
        {
            return Definition;
        }

        public void SetDefinition(AlgorithmDefinition definition)
        {
            this.Definition = definition;
            this._program = JintEngine.Compile(definition.Code, false);
        }
    }

}